package com.gorbas.springsocial.wicket;

import static com.gorbas.springsocial.wicket.OAuthEntryPoint.PAGE_PARAMETER_MODE;
import static com.gorbas.springsocial.wicket.OAuthEntryPoint.PAGE_PARAMETER_PROVIDER;

import java.util.List;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.wicket.Session;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.flow.RedirectToUrlException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.support.OAuth1ConnectionFactory;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth1.AuthorizedRequestToken;
import org.springframework.social.oauth1.OAuth1Operations;
import org.springframework.social.oauth1.OAuth1Parameters;
import org.springframework.social.oauth1.OAuth1Version;
import org.springframework.social.oauth1.OAuthToken;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.GrantType;
import org.springframework.social.oauth2.OAuth2Operations;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;

import com.gorbas.springsocial.wicket.OAuthEntryPoint.Mode;

@Service("authEntryPointController")
public class OAuthEntryPointController {

	@Autowired
	private ConnectionFactoryLocator connectionFactoryLocator;

	@Autowired
	private UsersConnectionRepository usersConnectionRepository;

	private static final String COOKIE_NAME = "WICKET_USER_ID";

	// OAuth related page parameters
	private static final String PAGE_PARAMETER_OAUTH_CODE = "code";
	private static final String PAGE_PARAMETER_OAUTH_VERIFIER = "oauth_verifier";

	void login(String providerId, String scope) {
		// Check if there is a cookie that can auto-login user
		for (Cookie cookie : getWebRequest().getCookies()) {
			if (cookie.getName().equals(COOKIE_NAME)) {
				String userId = cookie.getValue();
				ConnectionRepository connectionRepository = usersConnectionRepository.createConnectionRepository(userId);
				getAuthSession().setConnectionRepository(userId, connectionRepository);
			}
		}
		// Check if user is already logged in with this provider
		if (getAuthSession().getUserId() != null) {
			List<Connection<?>> connections = getAuthSession().getConnectionRepository().findConnections(providerId);
			if (!CollectionUtils.isEmpty(connections)) {
				for (Connection<?> connection : connections) {
					if (!connection.hasExpired()) {
						return;// there is no need to login! He is already
								// logged in..
					}
				}
			}
		}

		ConnectionFactory connectionFactory = connectionFactoryLocator.getConnectionFactory(providerId);
		String loginURL = buildOAuthUrl(connectionFactory, scope, null);
		throw new RedirectToUrlException(loginURL);
	}

	void authenticate(String providerId) {
		ConnectionFactory connectionFactory = connectionFactoryLocator.getConnectionFactory(providerId);
		if (connectionFactory instanceof OAuth1ConnectionFactory) {
			completeConnection((OAuth1ConnectionFactory) connectionFactory);
		} else if (connectionFactory instanceof OAuth2ConnectionFactory) {
			completeConnection((OAuth2ConnectionFactory) connectionFactory);
		} else {
			throw new RuntimeException(String.format("Unsupported type of Connection Factory %s for prodiver %s", connectionFactory.getClass().getSimpleName(), connectionFactory.getProviderId()));
		}
	}

	/**
	 * Remove the user from the session and also delete the cookie!
	 */
	void logout() {
		getAuthSession().logout();
		Cookie toRemove = new Cookie(COOKIE_NAME, "");
		toRemove.setMaxAge(0);
		getWebResponse().addCookie(toRemove);
	}

	private OAuthSessionIF getAuthSession() {
		Session session = Session.get();
		Assert.isInstanceOf(OAuthSessionIF.class, session, String.format("Session should implements %s", OAuthSessionIF.class));
		return (OAuthSessionIF) session;
	}

	private HttpServletResponse getWebResponse() {
		org.apache.wicket.protocol.http.servlet.ServletWebResponse res = (org.apache.wicket.protocol.http.servlet.ServletWebResponse) RequestCycle.get().getResponse();
		return res.getContainerResponse();
	}

	private HttpServletRequest getWebRequest() {
		org.apache.wicket.protocol.http.servlet.ServletWebRequest req = (org.apache.wicket.protocol.http.servlet.ServletWebRequest) RequestCycle.get().getRequest();
		return req.getContainerRequest();
	}

	// Helping methods
	/**
	 * Builds the provider URL to redirect the user to for connection
	 * authorization.
	 * 
	 * @param connectionFactory
	 *            the service provider's connection factory e.g.
	 *            FacebookConnectionFactory
	 * @param scope
	 *            the scope of authentication in case of OAuth2
	 * @param additionalParameters
	 *            parameters to add to the authorization URL.
	 * @return the URL to redirect the user to for authorization
	 * @throws IllegalArgumentException
	 *             if the connection factory is not OAuth1 based.
	 */
	private String buildOAuthUrl(ConnectionFactory<?> connectionFactory, String scope, MultiValueMap<String, String> additionalParameters) {
		if (connectionFactory instanceof OAuth1ConnectionFactory) {
			return buildOAuth1Url((OAuth1ConnectionFactory<?>) connectionFactory, additionalParameters);
		} else if (connectionFactory instanceof OAuth2ConnectionFactory) {
			return buildOAuth2Url((OAuth2ConnectionFactory<?>) connectionFactory, scope, additionalParameters);
		} else {
			throw new IllegalArgumentException("ConnectionFactory not supported");
		}
	}

	private void addConnection(Connection connection) {
		List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
		Assert.isTrue(userIds.size() == 1, String.format("You should not relate the account %s of the %s with %d user accounts, but only with one!", connection.getKey().getProviderUserId(),
				connection.getKey().getProviderId(), userIds.size()));
		String newConnectionUserId = userIds.get(0);
		Assert.isTrue((getAuthSession().getUserId() == null) || newConnectionUserId.equals(getAuthSession().getUserId()),
				String.format("Current user id should be related the same as the one the new connection! Session userId:%s Connection's userId:%s", getAuthSession().getUserId(), newConnectionUserId));
		// Login the user in case that (s)he is not already logged in
		if (getAuthSession().getUserId() == null) {
			ConnectionRepository connectionRepository = usersConnectionRepository.createConnectionRepository(newConnectionUserId);
			getAuthSession().setConnectionRepository(newConnectionUserId, connectionRepository);
			getWebResponse().addCookie(new Cookie(COOKIE_NAME, newConnectionUserId));
		}
	}

	/**
	 * Complete the connection to the OAuth1 provider and keep the Connection to
	 * the Session.
	 * 
	 * @param connectionFactory
	 *            the service provider's connection factory e.g.
	 *            FacebookConnectionFactory
	 * @return a new connection to the service provider
	 */
	private void completeConnection(OAuth1ConnectionFactory<?> connectionFactory) {
		String verifier = (String) getWebRequest().getAttribute(PAGE_PARAMETER_OAUTH_VERIFIER);
		AuthorizedRequestToken requestToken = new AuthorizedRequestToken(extractCachedRequestToken(), verifier);
		OAuthToken accessToken = connectionFactory.getOAuthOperations().exchangeForAccessToken(requestToken, null);
		Connection connection = connectionFactory.createConnection(accessToken);
		addConnection(connection);
	}

	/**
	 * Complete the connection to the OAuth2 provider and keep the Connection to
	 * the Session.
	 * 
	 * @param connectionFactory
	 *            the service provider's connection factory e.g.
	 *            FacebookConnectionFactory
	 * 
	 */
	private void completeConnection(OAuth2ConnectionFactory<?> connectionFactory) {
		String code = (String) getWebRequest().getAttribute(PAGE_PARAMETER_OAUTH_CODE);
		AccessGrant accessGrant = connectionFactory.getOAuthOperations().exchangeForAccess(code, callbackUrl(connectionFactory.getProviderId()), null);
		Connection connection = connectionFactory.createConnection(accessGrant);
		addConnection(connection);
	}

	private String buildOAuth1Url(OAuth1ConnectionFactory<?> connectionFactory, MultiValueMap<String, String> additionalParameters) {
		String providerId = connectionFactory.getProviderId();
		OAuth1Operations oauthOperations = connectionFactory.getOAuthOperations();
		OAuth1Parameters parameters = new OAuth1Parameters(additionalParameters);
		if (oauthOperations.getVersion() == OAuth1Version.CORE_10) {
			parameters.setCallbackUrl(callbackUrl(providerId));
		}
		OAuthToken requestToken = fetchRequestToken(providerId, oauthOperations);
		getAuthSession().setAuthToken(requestToken);
		return buildOAuth1Url(oauthOperations, requestToken.getValue(), parameters);
	}

	private OAuthToken fetchRequestToken(String providerId, OAuth1Operations oauthOperations) {
		if (oauthOperations.getVersion() == OAuth1Version.CORE_10_REVISION_A) {
			return oauthOperations.fetchRequestToken(callbackUrl(providerId), null);
		}
		return oauthOperations.fetchRequestToken(null, null);
	}

	private String buildOAuth2Url(OAuth2ConnectionFactory<?> connectionFactory, String scope, MultiValueMap<String, String> additionalParameters) {
		OAuth2Operations oauthOperations = connectionFactory.getOAuthOperations();
		OAuth2Parameters parameters = getOAuth2Parameters(additionalParameters, connectionFactory.getProviderId(), scope);
		return oauthOperations.buildAuthenticateUrl(GrantType.AUTHORIZATION_CODE, parameters);
	}

	private OAuth2Parameters getOAuth2Parameters(MultiValueMap<String, String> additionalParameters, String providerId, String scope) {
		OAuth2Parameters parameters = new OAuth2Parameters(additionalParameters);
		parameters.setRedirectUri(callbackUrl(providerId));
		if (scope != null) {
			parameters.setScope(scope);
		}
		return parameters;
	}

	private String callbackUrl(String providerId) {
		return String.format("%s?%s=%s&%s=%s", getWebRequest().getRequestURL().toString(), PAGE_PARAMETER_MODE, Mode.Authenticate.name(), PAGE_PARAMETER_PROVIDER, providerId);
	}

	private String connectPath() {
		HttpServletRequest request = getWebRequest();
		String pathInfo = request.getPathInfo();
		return request.getServletPath() + (pathInfo != null ? pathInfo : "");
	}

	private String buildOAuth1Url(OAuth1Operations oauthOperations, String requestToken, OAuth1Parameters parameters) {
		return oauthOperations.buildAuthenticateUrl(requestToken, parameters);
	}

	private OAuthToken extractCachedRequestToken() {
		OAuthToken requestToken = getAuthSession().getAuthToken();
		getAuthSession().setAuthToken(null);
		return requestToken;
	}
}
